了解完 GraalVM 是一個強大且多功能的虛擬機,能夠運行多種語言,並提供優越的性能和跨平台支持。然而,GraalVM 不僅僅是在 JVM 基礎上進行的優化,它還提供了一些突破性的技術,讓開發者能更靈活地部署和運行應用程序。
其中,Native Image 就是 GraalVM 的一個核心功能。當我們談到 GraalVM 的靈活性和性能優勢時,Native Image 提供了一條全新的路徑,將 Java 程式碼編譯成為原生二進制文件,不再依賴 JVM 運行。這帶來了許多傳統 Java 開發所無法實現的性能提升和運行優勢。
接下來,我們將介紹探討Native Image的運作方式與優勢。
Native Image 是一項將 Java 程式碼提前編譯成二進制文件(即原生可執行文件)的技術。與傳統的 Java 虛擬機器(JVM)運行方式不同,Native Image 僅包含在運行時所需的代碼,例如應用程序類、標準庫類、語言運行時以及來自 JDK 的靜態鏈接原生程式碼。
這種由 Native Image 編譯生成的可執行文件有以下幾個關鍵優點:
Graal編譯器除了可以即時編譯(JIT),還可以預先編譯(AOT),產生原生執行檔。考慮到Java的動態特性,這是怎麼做到的呢? 下圖為GraalVM Native Image 的運作方式
GraalVM Native Image 透過靜態分析(Points-to Analysis)
來判斷應用程式在運行時所需的類別和方法,並將這些程式碼在構建階段進行提前編譯(AOT)
。同時,它會預先初始化一些物件並進行堆快照
,將這些物件的狀態寫入映像檔,讓應用程式在啟動時可以快速載入。最終,生成的原生執行檔
不需要 JVM 支援,並依靠 Substrate VM
提供輕量的執行時環境來管理記憶體與執行緒。這種方式使應用程式體積更小,啟動更快。
綠色區塊代表 GraalVM Native Image 中關鍵的三個運作步驟,這些步驟會反覆進行,直到達到「固定點」(fixed point)
,即所有的程式碼路徑和初始化物件都被處理完畢。具體來說:
main
方法,逐步遞歸分析程式碼,找出所有間接可達的路徑。這個過程會持續,直到所有需要的程式碼都被分析完畢。這個分析確保生成的原生執行檔只包含必要的程式碼,避免多餘部分,減小執行檔的大小。這三個步驟是反覆運行的,因為指向分析的結果會影響初始化和堆快照的過程,而堆快照的結果又可能改變指向分析的內容。這個反覆過程會進行多次,直到所有程式碼路徑和初始化物件都被分析並處理完全,這時即達到「固定點」
,可進入下一步的編譯生成階段。
接著從綠色區塊往右邊,這部分描述了生成原生可執行檔的兩個關鍵步驟:
Ahead-of-Time Compilation(AOT 編譯):
當指向分析(Points-to Analysis)和初始化步驟完成後,GraalVM 會將所有已確認會在執行時使用的程式碼進行提前編譯(AOT)
。這個編譯過程是將 Java bytecode 編譯成與目標平台相容的機器碼。編譯後的程式碼會被存放在可執行檔的 Code in Text Section(文字區段中的程式碼部分)
,這是程式的主體邏輯。由於是提前編譯,因此在執行時不需要即時編譯(JIT),這減少了執行時的延遲,讓應用啟動速度更快。
Image Heap Writing(映像堆寫入):
在堆快照(Heap Snapshotting)完成後,應用程式的初始記憶體狀態會被寫入到映像檔中。這些預先配置的物件和數據會被存放在 Image Heap in Data Section(資料區段中的映像堆部分)
。當應用程式啟動時,它會直接載入這些記憶體數據,不需要重新初始化堆中的物件,進一步加快了啟動速度。
這個過程的最終結果是生成一個 原生可執行檔
,該執行檔是平台專屬的(根據你選擇的目標平台編譯),且不需要 Java 虛擬機(JVM)來運行。這個原生可執行檔包含了應用程式的所有必需程式碼和初始化好的記憶體狀態,既輕量又高效,並能提供更快的啟動時間與更低的資源消耗。
了解 Native Image 的運作對 Quarkus 開發非常有幫助,因為 Quarkus 的核心特性之一就是其對 GraalVM 的原生支援。透過 Native Image,Quarkus 應用程式能夠顯著提升效能,特別是在以下幾個方面:
因此,理解和善用 Native Image 的運作原理,對於 Quarkus 開發者來說,能夠充分發揮 Quarkus 在啟動速度、資源效率和運行效能上的優勢,使其成為在微服務、容器化應用或雲原生環境中強大的開發工具。
在開發雲原生應用程式時,使用 GraalVM Native Image 雖然可以顯著降低應用的記憶體使用量並提升啟動速度。但由於 Native Image 採用了靜態編譯方法,與傳統 JVM 相比,開發者在使用過程中會遇到一些不同的挑戰和問題。以下是一些常見的 Native Image 相關問題及其解決方案:
長時間的構建過程:
Native Image 構建過程比普通 JVM 編譯要慢得多,尤其是當應用程式規模較大時,編譯時間會顯著增長。這是因為 GraalVM 進行了大量的靜態分析和優化。
解決方案:利用 Quarkus 提供的快速開發模式,開發過程中先以 JVM 模式進行開發,僅在需要時使用 Native Image 編譯。可以通過 CI/CD 管道進行自動編譯,將這部分負擔轉移到後端。
第三方函式庫相容性問題:
並非所有的 Java 函式庫都與 Native Image 相容,特別是那些依賴於反射、動態代理或特定 JVM 特性的函式庫。
解決方案:選擇已知與 GraalVM Native Image 相容的函式庫,或尋找替代方案。此外,檢查函式庫的官方文件,了解它們是否提供 Native Image 支援。
動態特性支援不足:
Java 的動態功能(例如反射、動態代理、序列化等)在 Native Image 中支援有限。由於 Native Image 使用靜態分析來確定可達程式碼,動態加載的類別可能會被排除。開發者需要使用特定的配置文件來告知 Native Image 需要保留這些類別和方法。
解決方案:需要手動配置 reflect-config.json
或使用 Quarkus 的 @RegisterForReflection
註解來明確告知哪些類別需要在反射中使用。
Native Image 的生成過程需要直接生成與作業系統和硬體相容的機器碼,這部分需要 C++ 編譯器來處理,特別是在系統呼叫、記憶體管理等底層功能上。而JVM 則內建了許多像是記憶體管理、垃圾回收、執行緒管理等功能,它們全由 JVM 自己處理,因此不需要依賴外部的 C++ 編譯器來生成原生機器碼。JVM 處理所有 Java bytecode 的解釋和運行,負責與作業系統交互。